package net.cathackers.devel.scmp.chat.impl.xmpp;

import java.io.IOException;
import java.io.StringReader;

import org.apache.log4j.Logger;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.io.SAXReader;

import net.cathackers.devel.scmp.chat.interfaces.ChatManager;
import net.cathackers.devel.scmp.chat.interfaces.ChatSession;
import net.cathackers.devel.scmp.chat.interfaces.XMPPChatManager;
import net.cathackers.devel.scmp.chat.interfaces.XMPPConnection;
import net.cathackers.devel.scmp.config.impl.SCMPConstants;
import net.cathackers.devel.scmp.factories.SCMPFactory;
import net.cathackers.devel.scmp.xmpp.exceptions.UnknownPacket;

public class XMPPProtocolParser {
    private XMPPConnection            connection_;
    private boolean                   tlsStarted_;
    private boolean                   saslStarted_;
    private boolean                   sessionCreated_;
    private SASLAuthentication.Status saslStatus_;
    private Logger                    logger_;
    SAXReader                         reader_;
    // Indicate if a stream:stream is arrived to complete compression
    private boolean                   waitingCompressionACK_ = false;
    private ChatManager		      chatManager_;
    /**
     * Session associated with the socket reader.
     */
    protected ChatSession             session_;
    /**
     * Server name for which we are attending clients.
     */
    protected String                  serverName_;

    public XMPPProtocolParser(String serverName, XMPPConnection connection) {
        this.serverName_ = serverName;
        this.chatManager_ = SCMPFactory.getChatManager();

        this.connection_ = connection;
        this.logger_ = SCMPFactory.getLogger(this);
        this.reader_ = new SAXReader();
    }

    public void process(String stanza) {
        Document doc;
        // Ignore <?xml version="1.0"?> stanzas sent by clients
        if (stanza.startsWith("<?xml")) { // the XML icing :)
            return;
        }
        boolean initialStream = (stanza.startsWith("<stream:stream") || stanza.startsWith("<flash:stream"));
        if (!sessionCreated_ || initialStream) {
            if (!initialStream) {
                // Ignore <?xml version="1.0"?>
                return;
            }
            // Found an stream:stream tag...
            if (!sessionCreated_) {
                logger_.debug("initial stream, creating session....");
                createSession(stanza);
                sessionCreated_ = true;
            } else if (tlsStarted_) {
                tlsStarted_ = false; // tls negotiation is finished in fact.
                tlsNegotiationFinished();
            } else if (saslStarted_ && saslStatus_ == SASLAuthentication.Status.authenticated) {
                saslStarted_ = false; // SASL authentication was successful
                saslSuccessful();
            } else if (waitingCompressionACK_) {
                waitingCompressionACK_ = false;
                compressionSuccessful();
            }
            return;
        }

        // bye bye?
        if (stanza.equals("</stream:stream>")) {
            logger_.debug("the parser received this packet " + stanza);
           // chatManager_.unregisterSession(session_);
            session_.bye();
            return;
        }
        try {
            reader_.setValidation(false);
            doc = reader_.read(new StringReader(stanza));
        } catch (DocumentException ex) {
            // error parsing the document....
            logger_.error("Error parsing stanza '" + stanza + "':" + ex.getMessage());
            return;
        }
        if (doc == null) {
            // No document found.
            return;
        }
        String tag = doc.getRootElement().getName();
        if ("starttls".equals(tag)) {
            // Negotiate TLS
            if (negotiateTLS()) {
                tlsStarted_ = true;
            } else {
                logger_.error("Couldn't negotiate TLS for session!");
                connection_.close();
                session_ = null;
            }
        } else if ("auth".equals(tag)) {
            // User is trying to authenticate using SASL
            saslStarted_ = true;
            logger_.debug("Started SASL");
            // Process authentication stanza
            // saslStatus = SASLAuthentication.handle(session_, doc); //initiate
            // SASL challenge
        } else if (saslStarted_ && "response".equals(tag)) {
            logger_.debug("User responded to SASL Challenge");
            // User is responding to SASL challenge. Process response
            // saslStatus = SASLAuthentication.handle(session, doc);
        } else if ("compress".equals(tag)) {
            // Client is trying to initiate compression
            if (compressClient(doc)) {
                // Compression was successful so open a new stream and offer
                // resource binding and session establishment (to client
                // sessions only)
                waitingCompressionACK_ = true;
            }
        } else {
            process(doc);
        }
    }

    private void process(Document doc) {
        if (doc == null) {
            return;
        }

        // Ensure that connection was secured if TLS was required
        if (connection_.getTLSPolicy() == XMPPConnection.TLSPolicy.required && !connection_.isTLS()) {
            // Set the not_authorized error
            XMPPError error = new XMPPError(XMPPError.Condition.not_authorized);
            connection_.deliverString(error.toXML());
            logger_.warn("Not authorized to send stanzas by " + connection_);
            connection_.close();
            // Log a warning so that admins can track this case from the server
            // side
            logger_.warn("TLS was required by the server and connection was never secured. " + "Closing connection : " + connection_);
            return;
        }

        try {
            logger_.debug("Routing Message  " + doc.asXML());
            logger_.debug("Routing Message for session " + connection_.getSessionID());
            chatManager_.route(doc, connection_.getSessionID());
        } catch (UnknownPacket e) {
            session_.bye();
        }
    }

    private boolean compressClient(Document doc) {
        // We are trying to handle all the possible cases here, so be patient
        // please :)
        String error = null;
        boolean ret = false;
        if (connection_.getCompressionPolicy() == XMPPConnection.CompressionPolicy.disabled) {
            // Client requested compression but this feature is disabled
            error = "<failure xmlns='http://jabber.org/protocol/compress'><setup-failed/></failure>";
            logger_.warn("Client requested compression while compression is disabled. Closing " + "connection : " + connection_);
        } else if (connection_.isCompressed()) {
            // Client requested compression but connection is already compressed
            error = "<failure xmlns='http://jabber.org/protocol/compress'><setup-failed/></failure>";
            logger_.warn("Client requested compression and connection is already compressed. Closing " + "connection : " + connection_);
        } else {
            // Check that the requested method is supported, currently we only
            // support zlib
            String method = doc.getRootElement().elementText("method");
            if (!"zlib".equals(method)) {
                error = "<failure xmlns='http://jabber.org/protocol/compress'><unsupported-method/></failure>";
                // Log a warning so that admins can track this case from the
                // server side
                logger_.warn("Requested compression method is not supported: " + method + ". Closing connection : " + connection_);
            }
        }

        if (error != null) {
            // Deliver stanza
            connection_.deliverString(error);
            ret = false;
        } else {
            // Start using compression for incoming traffic
            connection_.addCompression();
            connection_.deliverString("<compressed xmlns='http://jabber.org/protocol/compress'/>");
            // Start using compression for outgoing traffic
            connection_.startCompression();
            ret = true;
        }
        return ret;
    }

    private void startTLS() throws IOException {
        try {
	    connection_.startTLS(false, null, XMPPConnection.ClientAuth.disabled);
	} catch (Exception e) {
	    logger_.error("Cloudn't start TLS", e);
            connection_.deliverString("<failure xmlns=\"urn:ietf:params:xml:ns:xmpp-tls\">");
            connection_.close();
	}
    }

    private boolean negotiateTLS() {
        if (connection_.getTLSPolicy() == XMPPConnection.TLSPolicy.disabled) {
            XMPPError error = new XMPPError(XMPPError.Condition.not_authorized);
            connection_.deliverString(error.toXML());
            // Close the underlying connection
            connection_.close();
            // Log a warning so that admins can track this case from the server
            // side
            logger_.warn("TLS requested by initiator when TLS was never offered by server. " + "Closing connection : " + connection_);
            return false;
        }
        // Client requested to secure the connection using TLS. Negotiate TLS.
        try {
            startTLS();
        } catch (IOException e) {
            logger_.error("Error while negotiating TLS", e);
            connection_.deliverString("<failure xmlns=\"urn:ietf:params:xml:ns:xmpp-tls\">");
            connection_.close();
            return false;
        }
        return true;
    }

    private void compressionSuccessful() {
        StringBuilder sb = new StringBuilder(340);
        sb.append(getStreamHeader());
        sb.append("<stream:features>");
        // Include SASL mechanisms only if client has not been authenticated
        if (session_.getStatus() != ChatSession.STATUS_AUTHENTICATED) {
            // Include available SASL Mechanisms
            // sb.append(SASLAuthentication.getSASLMechanisms(session));
        }
        // Include specific features such as resource binding and session
        // establishment
        // for client sessions
        String specificFeatures = session_.getPossibleStreamFeatures();
        if (specificFeatures != null) {
            sb.append(specificFeatures);
        }
        sb.append("</stream:features>");
        // Shoot....
        connection_.deliverString(sb.toString());

    }
    public XMPPConnection getConnection() {
	return connection_;
    }
    private String getStreamHeader() {
        StringBuilder sb = new StringBuilder(200);
        sb.append("<?xml version='1.0' encoding='");
        sb.append(SCMPConstants._ENCODING_);
        sb.append("'?>");
        if (connection_.isFlashClient()) { // We are supporting Spark flash
                                           // client.
            sb.append("<flash:stream xmlns:flash=\"http://www.jabber.com/streams/flash\" ");
        } else {
            sb.append("<stream:stream ");
        }
        sb.append("xmlns:stream=\"http://etherx.jabber.org/streams\" xmlns=\"");
        sb.append("jabber:client"); // Should be parametrized if we will be
                                    // communicating with non-clients.
        sb.append("\" from=\"");
        sb.append(serverName_);
        sb.append("\" id=\"");
        sb.append(session_.getSessionID());
        sb.append("\" xml:lang=\"");
        sb.append(connection_.getLanguage());
        sb.append("\" version=\"");
        sb.append(ChatSession.MAJOR_VERSION).append(".").append(ChatSession.MINOR_VERSION);
        sb.append("\">");
        return sb.toString();
    }

    private void saslSuccessful() {
        // TODO Auto-generated method stub

    }

    public void createSession(String initDoc) {
        Document doc;
        try {
            // hack the message to be XML well-formed
            initDoc = initDoc.replaceAll(">", "/>");
            logger_.debug("Message converted to:" + initDoc);
            doc = reader_.read(new StringReader(initDoc));
        } catch (DocumentException ex) {
            // error parsing the document....
            logger_.error("Error parsing stanza '" + initDoc + "':" + ex.getMessage());
            return;
        }
        if (doc == null) {
            // No document found.
            return;
        }
        // Create the correct session based on the sent namespace. At this point
        // the server
        // may offer the client to secure the connection. If the client decides
        // to secure
        // the connection then a <starttls> stanza should be received
        if (!createSession(serverName_, doc, connection_)) {
            // No session was created because of an invalid namespace prefix so
            // answer a stream
            // error and close the underlying connection
            StringBuilder sb = new StringBuilder(250);
            sb.append("<?xml version='1.0' encoding='");
            sb.append(SCMPConstants._ENCODING_);
            sb.append("'?>");
            // Append stream header
            sb.append("<stream:stream ");
            sb.append("from=\"").append(serverName_).append("\" ");
            sb.append("id=\"").append(XMPPChatManager.generateRandomString(5)).append("\" ");
            sb.append("xmlns=\"").append(doc.getRootElement().getNamespaceForPrefix("").getText()).append("\" ");
            sb.append("xmlns:stream=\"").append(doc.getRootElement().getNamespaceForPrefix("stream").getText()).append("\" ");
            sb.append("version=\"1.0\">");
            // Include the bad-namespace-prefix in the response
            XMPPError error = new XMPPError(XMPPError.Condition.bad_namespace_prefix);
            sb.append(error.toXML());
            connection_.deliverString(sb.toString());
            // Close the underlying connection
            connection_.close();
            // Log a warning so that admins can track this cases from the server
            // side
            logger_.warn("Closing session due to bad_namespace_prefix in stream header. Prefix: " + doc.getRootElement().getNamespaceForPrefix("").getText()
                    + ". Connection: " + connection_);
        }
    }

    private boolean createSession(String serverName, Document initDoc, XMPPConnection connection) {
        String namespace = initDoc.getRootElement().getNamespaceForPrefix("").getText();
        if ("jabber:client".equals(namespace)) {
            // The connected client is a regular client so create a
            // ClientSession
            session_ = this.chatManager_.createSession(serverName, initDoc, connection);
            return true;
        }
        return false;
    }

    public void tlsNegotiationFinished() {
        // Offer stream features including SASL Mechanisms
        StringBuilder sb = new StringBuilder(620);
        sb.append(getStreamHeader());
        sb.append("<stream:features>");
        // Include available SASL Mechanisms
//        sb.append(SASLAuthentication.getSASLMechanisms(session_));
        // Include specific features such as auth and register for client
        // sessions
        String specificFeatures = session_.getPossibleStreamFeatures();
        if (specificFeatures != null) {
            sb.append(specificFeatures);
        }
        sb.append("</stream:features>");
        connection_.deliverString(sb.toString());
    }
}
